package json

import (
    "encoding/json"
    "fmt"

    "Havoc/pkg/profile/yaotl"
    "github.com/zclconf/go-cty/cty"
)

func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
    tokens := scan(buf, pos{Filename: filename, Pos: start})
    p := newPeeker(tokens)
    node, diags := parseValue(p)
    if len(diags) == 0 && p.Peek().Type != tokenEOF {
        diags = diags.Append(&hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Extraneous data after value",
            Detail:   "Extra characters appear after the JSON value.",
            Subject:  p.Peek().Range.Ptr(),
        })
    }
    return node, diags
}

func parseExpression(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
    tokens := scan(buf, pos{Filename: filename, Pos: start})
    p := newPeeker(tokens)
    node, diags := parseValue(p)
    if len(diags) == 0 && p.Peek().Type != tokenEOF {
        diags = diags.Append(&hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Extraneous data after value",
            Detail:   "Extra characters appear after the JSON value.",
            Subject:  p.Peek().Range.Ptr(),
        })
    }
    return node, diags
}

func parseValue(p *peeker) (node, hcl.Diagnostics) {
    tok := p.Peek()

    wrapInvalid := func(n node, diags hcl.Diagnostics) (node, hcl.Diagnostics) {
        if n != nil {
            return n, diags
        }
        return invalidVal{tok.Range}, diags
    }

    switch tok.Type {
    case tokenBraceO:
        return wrapInvalid(parseObject(p))
    case tokenBrackO:
        return wrapInvalid(parseArray(p))
    case tokenNumber:
        return wrapInvalid(parseNumber(p))
    case tokenString:
        return wrapInvalid(parseString(p))
    case tokenKeyword:
        return wrapInvalid(parseKeyword(p))
    case tokenBraceC:
        return wrapInvalid(nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Missing JSON value",
                Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
                Subject:  &tok.Range,
            },
        })
    case tokenBrackC:
        return wrapInvalid(nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Missing array element value",
                Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
                Subject:  &tok.Range,
            },
        })
    case tokenEOF:
        return wrapInvalid(nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Missing value",
                Detail:   "The JSON data ends prematurely.",
                Subject:  &tok.Range,
            },
        })
    default:
        return wrapInvalid(nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid start of value",
                Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
                Subject:  &tok.Range,
            },
        })
    }
}

func tokenCanStartValue(tok token) bool {
    switch tok.Type {
    case tokenBraceO, tokenBrackO, tokenNumber, tokenString, tokenKeyword:
        return true
    default:
        return false
    }
}

func parseObject(p *peeker) (node, hcl.Diagnostics) {
    var diags hcl.Diagnostics

    open := p.Read()
    attrs := []*objectAttr{}

    // recover is used to shift the peeker to what seems to be the end of
    // our object, so that when we encounter an error we leave the peeker
    // at a reasonable point in the token stream to continue parsing.
    recover := func(tok token) {
        open := 1
        for {
            switch tok.Type {
            case tokenBraceO:
                open++
            case tokenBraceC:
                open--
                if open <= 1 {
                    return
                }
            case tokenEOF:
                // Ran out of source before we were able to recover,
                // so we'll bail here and let the caller deal with it.
                return
            }
            tok = p.Read()
        }
    }

Token:
    for {
        if p.Peek().Type == tokenBraceC {
            break Token
        }

        keyNode, keyDiags := parseValue(p)
        diags = diags.Extend(keyDiags)
        if keyNode == nil {
            return nil, diags
        }

        keyStrNode, ok := keyNode.(*stringVal)
        if !ok {
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Invalid object property name",
                Detail:   "A JSON object property name must be a string",
                Subject:  keyNode.StartRange().Ptr(),
            })
        }

        key := keyStrNode.Value

        colon := p.Read()
        if colon.Type != tokenColon {
            recover(colon)

            if colon.Type == tokenBraceC || colon.Type == tokenComma {
                // Catch common mistake of using braces instead of brackets
                // for an object.
                return nil, diags.Append(&hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  "Missing object value",
                    Detail:   "A JSON object attribute must have a value, introduced by a colon.",
                    Subject:  &colon.Range,
                })
            }

            if colon.Type == tokenEquals {
                // Possible confusion with native HCL syntax.
                return nil, diags.Append(&hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  "Missing property value colon",
                    Detail:   "JSON uses a colon as its name/value delimiter, not an equals sign.",
                    Subject:  &colon.Range,
                })
            }

            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Missing property value colon",
                Detail:   "A colon must appear between an object property's name and its value.",
                Subject:  &colon.Range,
            })
        }

        valNode, valDiags := parseValue(p)
        diags = diags.Extend(valDiags)
        if valNode == nil {
            return nil, diags
        }

        attrs = append(attrs, &objectAttr{
            Name:      key,
            Value:     valNode,
            NameRange: keyStrNode.SrcRange,
        })

        switch p.Peek().Type {
        case tokenComma:
            comma := p.Read()
            if p.Peek().Type == tokenBraceC {
                // Special error message for this common mistake
                return nil, diags.Append(&hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  "Trailing comma in object",
                    Detail:   "JSON does not permit a trailing comma after the final property in an object.",
                    Subject:  &comma.Range,
                })
            }
            continue Token
        case tokenEOF:
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Unclosed object",
                Detail:   "No closing brace was found for this JSON object.",
                Subject:  &open.Range,
            })
        case tokenBrackC:
            // Consume the bracket anyway, so that we don't return with the peeker
            // at a strange place.
            p.Read()
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Mismatched braces",
                Detail:   "A JSON object must be closed with a brace, not a bracket.",
                Subject:  p.Peek().Range.Ptr(),
            })
        case tokenBraceC:
            break Token
        default:
            recover(p.Read())
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Missing attribute separator comma",
                Detail:   "A comma must appear between each property definition in an object.",
                Subject:  p.Peek().Range.Ptr(),
            })
        }

    }

    close := p.Read()
    return &objectVal{
        Attrs:      attrs,
        SrcRange:   hcl.RangeBetween(open.Range, close.Range),
        OpenRange:  open.Range,
        CloseRange: close.Range,
    }, diags
}

func parseArray(p *peeker) (node, hcl.Diagnostics) {
    var diags hcl.Diagnostics

    open := p.Read()
    vals := []node{}

    // recover is used to shift the peeker to what seems to be the end of
    // our array, so that when we encounter an error we leave the peeker
    // at a reasonable point in the token stream to continue parsing.
    recover := func(tok token) {
        open := 1
        for {
            switch tok.Type {
            case tokenBrackO:
                open++
            case tokenBrackC:
                open--
                if open <= 1 {
                    return
                }
            case tokenEOF:
                // Ran out of source before we were able to recover,
                // so we'll bail here and let the caller deal with it.
                return
            }
            tok = p.Read()
        }
    }

Token:
    for {
        if p.Peek().Type == tokenBrackC {
            break Token
        }

        valNode, valDiags := parseValue(p)
        diags = diags.Extend(valDiags)
        if valNode == nil {
            return nil, diags
        }

        vals = append(vals, valNode)

        switch p.Peek().Type {
        case tokenComma:
            comma := p.Read()
            if p.Peek().Type == tokenBrackC {
                // Special error message for this common mistake
                return nil, diags.Append(&hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  "Trailing comma in array",
                    Detail:   "JSON does not permit a trailing comma after the final value in an array.",
                    Subject:  &comma.Range,
                })
            }
            continue Token
        case tokenColon:
            recover(p.Read())
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Invalid array value",
                Detail:   "A colon is not used to introduce values in a JSON array.",
                Subject:  p.Peek().Range.Ptr(),
            })
        case tokenEOF:
            recover(p.Read())
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Unclosed object",
                Detail:   "No closing bracket was found for this JSON array.",
                Subject:  &open.Range,
            })
        case tokenBraceC:
            recover(p.Read())
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Mismatched brackets",
                Detail:   "A JSON array must be closed with a bracket, not a brace.",
                Subject:  p.Peek().Range.Ptr(),
            })
        case tokenBrackC:
            break Token
        default:
            recover(p.Read())
            return nil, diags.Append(&hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Missing attribute separator comma",
                Detail:   "A comma must appear between each value in an array.",
                Subject:  p.Peek().Range.Ptr(),
            })
        }

    }

    close := p.Read()
    return &arrayVal{
        Values:    vals,
        SrcRange:  hcl.RangeBetween(open.Range, close.Range),
        OpenRange: open.Range,
    }, diags
}

func parseNumber(p *peeker) (node, hcl.Diagnostics) {
    tok := p.Read()

    // Use encoding/json to validate the number syntax.
    // TODO: Do this more directly to produce better diagnostics.
    var num json.Number
    err := json.Unmarshal(tok.Bytes, &num)
    if err != nil {
        return nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid JSON number",
                Detail:   fmt.Sprintf("There is a syntax error in the given JSON number."),
                Subject:  &tok.Range,
            },
        }
    }

    // We want to guarantee that we parse numbers the same way as cty (and thus
    // native syntax HCL) would here, so we'll use the cty parser even though
    // in most other cases we don't actually introduce cty concepts until
    // decoding time. We'll unwrap the parsed float immediately afterwards, so
    // the cty value is just a temporary helper.
    nv, err := cty.ParseNumberVal(string(num))
    if err != nil {
        // Should never happen if above passed, since JSON numbers are a subset
        // of what cty can parse...
        return nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid JSON number",
                Detail:   fmt.Sprintf("There is a syntax error in the given JSON number."),
                Subject:  &tok.Range,
            },
        }
    }

    return &numberVal{
        Value:    nv.AsBigFloat(),
        SrcRange: tok.Range,
    }, nil
}

func parseString(p *peeker) (node, hcl.Diagnostics) {
    tok := p.Read()
    var str string
    err := json.Unmarshal(tok.Bytes, &str)

    if err != nil {
        var errRange hcl.Range
        if serr, ok := err.(*json.SyntaxError); ok {
            errOfs := serr.Offset
            errPos := tok.Range.Start
            errPos.Byte += int(errOfs)

            // TODO: Use the byte offset to properly count unicode
            // characters for the column, and mark the whole of the
            // character that was wrong as part of our range.
            errPos.Column += int(errOfs)

            errEndPos := errPos
            errEndPos.Byte++
            errEndPos.Column++

            errRange = hcl.Range{
                Filename: tok.Range.Filename,
                Start:    errPos,
                End:      errEndPos,
            }
        } else {
            errRange = tok.Range
        }

        var contextRange *hcl.Range
        if errRange != tok.Range {
            contextRange = &tok.Range
        }

        // FIXME: Eventually we should parse strings directly here so
        // we can produce a more useful error message in the face fo things
        // such as invalid escapes, etc.
        return nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid JSON string",
                Detail:   fmt.Sprintf("There is a syntax error in the given JSON string."),
                Subject:  &errRange,
                Context:  contextRange,
            },
        }
    }

    return &stringVal{
        Value:    str,
        SrcRange: tok.Range,
    }, nil
}

func parseKeyword(p *peeker) (node, hcl.Diagnostics) {
    tok := p.Read()
    s := string(tok.Bytes)

    switch s {
    case "true":
        return &booleanVal{
            Value:    true,
            SrcRange: tok.Range,
        }, nil
    case "false":
        return &booleanVal{
            Value:    false,
            SrcRange: tok.Range,
        }, nil
    case "null":
        return &nullVal{
            SrcRange: tok.Range,
        }, nil
    case "undefined", "NaN", "Infinity":
        return nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid JSON keyword",
                Detail:   fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s),
                Subject:  &tok.Range,
            },
        }
    default:
        var dym string
        if suggest := keywordSuggestion(s); suggest != "" {
            dym = fmt.Sprintf(" Did you mean %q?", suggest)
        }

        return nil, hcl.Diagnostics{
            {
                Severity: hcl.DiagError,
                Summary:  "Invalid JSON keyword",
                Detail:   fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym),
                Subject:  &tok.Range,
            },
        }
    }
}
